1 package edu.jiangxin.apktoolbox.convert.protobuf.supervised; 2 3 import com.google.protobuf.DescriptorProtos; 4 import com.google.protobuf.Descriptors; 5 import com.google.protobuf.InvalidProtocolBufferException; 6 7 import java.io.IOException; 8 import java.io.UncheckedIOException; 9 import java.nio.file.Files; 10 import java.nio.file.Path; 11 import java.util.*; 12 import java.util.stream.Stream; 13 14 /** 15 * Cache containing protobuf descriptors used by {@link ProtoToJson} in order to decode binary protobuf messages. 16 * <p> 17 * An instance can be created using the static factory methods {@link #emptyCache()}, {@link #fromDirectory(Path)} and 18 * {@link #fromFile(Path)}. 19 * <p> 20 * Additional descriptors can be added using {@link #addDescriptor(Descriptors.Descriptor)}, {@link 21 * #addDescriptors(Path)} and {@link #addDescriptors(byte[])}. They can be received using {@link #getByTypeName(String)} 22 * and {@link #getDescriptors()}. 23 * <p> 24 * Descriptors can be obtained by applying a {@code protoc} command on the protobuf schema {@code .proto}, for example: 25 * <pre>{@code 26 * protoc --descriptor_set_out foo.desc foo.proto 27 * }</pre> 28 * 29 * @author Daniel Tischner {@literal <zabuza.dev@gmail.com>} 30 */ 31 public final class DescriptorCache { 32 private static final Descriptors.FileDescriptor[] DEPENDENCIES = new Descriptors.FileDescriptor[0]; 33 34 /** 35 * Creates an instance that initially has no descriptors. 36 * 37 * @return The created instance 38 */ 39 public static DescriptorCache emptyCache() { 40 return new DescriptorCache(); 41 } 42 43 /** 44 * Creates an instance from a directory containing descriptor files. 45 * <p> 46 * The directory must not contain files that are not valid descriptor files. 47 * <p> 48 * Descriptor files can be obtained by applying a {@code protoc} command on the protobuf schema {@code .proto}, for 49 * example: 50 * <pre>{@code 51 * protoc --descriptor_set_out foo.desc foo.proto 52 * }</pre> 53 * 54 * @param directory The directory containing the descriptor files, not null. 55 * 56 * @return The created instance that has all descriptors available in the given directory 57 * 58 * @throws IllegalArgumentException If the {@code directory} is not a directory 59 * @throws UncheckedIOException If an I/O error occurs during reading the files 60 * @throws UncheckedInvalidProtocolBufferException If a file is not a valid descriptor file 61 * @throws UncheckedDescriptorValidationException If a file contains malformed descriptors 62 */ 63 public static DescriptorCache fromDirectory(final Path directory) { 64 Objects.requireNonNull(directory); 65 66 if (!Files.isDirectory(directory)) { 67 throw new IllegalArgumentException("Path must be a directory: " + directory); 68 } 69 70 final DescriptorCache cache = new DescriptorCache(); 71 try (final Stream<Path> walk = Files.walk(directory)) { 72 walk.filter(Files::isRegularFile) 73 .forEach(cache::addDescriptors); 74 } catch (final IOException e) { 75 throw new UncheckedIOException(e); 76 } 77 return cache; 78 } 79 80 /** 81 * Creates an instance from a single descriptor file. 82 * <p> 83 * Descriptor files can be obtained by applying a {@code protoc} command on the protobuf schema {@code .proto}, for 84 * example: 85 * <pre>{@code 86 * protoc --descriptor_set_out foo.desc foo.proto 87 * }</pre> 88 * 89 * @param descriptorsFile The descriptor file, not null. 90 * 91 * @return The created instance that has all descriptors available in the given file 92 * 93 * @throws IllegalArgumentException If the {@code descriptorsFile} is not a regular file 94 * @throws UncheckedIOException If an I/O error occurs during reading the files 95 * @throws UncheckedInvalidProtocolBufferException If the file is not a valid descriptor file 96 * @throws UncheckedDescriptorValidationException If the file contains malformed descriptors 97 */ 98 public static DescriptorCache fromFile(final Path descriptorsFile) { 99 Objects.requireNonNull(descriptorsFile); 100 101 if (!Files.isRegularFile(descriptorsFile)) { 102 throw new IllegalArgumentException("Path must be a regular file: " + descriptorsFile); 103 } 104 final DescriptorCache cache = new DescriptorCache(); 105 cache.addDescriptors(descriptorsFile); 106 return cache; 107 } 108 109 /** 110 * Maps message type names to the descriptor suitable for parsing messages of that type, not null. 111 */ 112 private final Map<String, Descriptors.Descriptor> typeNameToDescriptor = new HashMap<>(); 113 114 /** 115 * Creates a new instance with no descriptors. 116 */ 117 private DescriptorCache() { 118 119 } 120 121 /** 122 * Adds the given descriptor to the cache. 123 * <p> 124 * Overwriting any descriptor previously registered for the same message type. 125 * 126 * @param descriptor The descriptor to add, not null 127 * 128 * @return The descriptor previously associated to the message type, if any 129 */ 130 @SuppressWarnings({ "WeakerAccess", "UnusedReturnValue" }) 131 public Optional<Descriptors.Descriptor> addDescriptor(final Descriptors.Descriptor descriptor) { 132 Objects.requireNonNull(descriptor); 133 final String typeName = Objects.requireNonNull(descriptor.getName()); 134 135 return Optional.ofNullable(typeNameToDescriptor.put(typeName, descriptor)); 136 } 137 138 /** 139 * Adds all descriptors given in a descriptor file to the cache. 140 * <p> 141 * Overwriting any descriptors previously registered for the same message types. 142 * <p> 143 * Descriptor files can be obtained by applying a {@code protoc} command on the protobuf schema {@code .proto}, for 144 * example: 145 * <pre>{@code 146 * protoc --descriptor_set_out foo.desc foo.proto 147 * }</pre> 148 * 149 * @param descriptorsFile The descriptor file, not null. 150 * 151 * @throws IllegalArgumentException If the {@code descriptorsFile} is not a regular file 152 * @throws UncheckedIOException If an I/O error occurs during reading the file 153 * @throws UncheckedInvalidProtocolBufferException If the file is not a valid descriptor file 154 * @throws UncheckedDescriptorValidationException If the file contains malformed descriptors 155 */ 156 @SuppressWarnings("WeakerAccess") 157 public void addDescriptors(final Path descriptorsFile) { 158 Objects.requireNonNull(descriptorsFile); 159 160 if (!Files.isRegularFile(descriptorsFile)) { 161 throw new IllegalArgumentException("Path must be a regular file: " + descriptorsFile); 162 } 163 164 try { 165 addDescriptors(Files.readAllBytes(descriptorsFile)); 166 } catch (final IOException e) { 167 throw new UncheckedIOException("While reading: " + descriptorsFile.toAbsolutePath(), e); 168 } 169 } 170 171 /** 172 * Adds all descriptors given as a raw descriptor set to the cache. 173 * <p> 174 * Overwriting any descriptors previously registered for the same message types. 175 * 176 * @param descriptorsRaw The raw descriptor set, not null. 177 * 178 * @throws UncheckedInvalidProtocolBufferException If the file is not a valid descriptor file 179 * @throws UncheckedDescriptorValidationException If the file contains malformed descriptors 180 */ 181 @SuppressWarnings("WeakerAccess") 182 public void addDescriptors(final byte[] descriptorsRaw) { 183 Objects.requireNonNull(descriptorsRaw); 184 185 try { 186 final DescriptorProtos.FileDescriptorSet descriptorSet = 187 DescriptorProtos.FileDescriptorSet.parseFrom(descriptorsRaw); 188 for (final DescriptorProtos.FileDescriptorProto descriptorFile : descriptorSet.getFileList()) { 189 final Descriptors.FileDescriptor fileDescriptor = 190 Descriptors.FileDescriptor.buildFrom(descriptorFile, DescriptorCache.DEPENDENCIES); 191 for (final Descriptors.Descriptor descriptor : fileDescriptor.getMessageTypes()) { 192 addDescriptor(descriptor); 193 } 194 } 195 } catch (final InvalidProtocolBufferException e) { 196 throw new UncheckedInvalidProtocolBufferException(e); 197 } catch (final Descriptors.DescriptorValidationException e) { 198 throw new UncheckedDescriptorValidationException(e); 199 } 200 } 201 202 /** 203 * Gets the descriptor registered for the given message type name, if any. 204 * 205 * @param typeName The message type name, not null 206 * 207 * @return The descriptor registered for the given message type name, if any 208 */ 209 public Optional<Descriptors.Descriptor> getByTypeName(final String typeName) { 210 Objects.requireNonNull(typeName); 211 212 return Optional.ofNullable(typeNameToDescriptor.get(typeName)); 213 } 214 215 /** 216 * Gets all descriptors registered by this cache. 217 * 218 * @return An unmodifiable collection of all registered descriptors 219 */ 220 public Collection<Descriptors.Descriptor> getDescriptors() { 221 return Collections.unmodifiableCollection(typeNameToDescriptor.values()); 222 } 223 224 /** 225 * Gets all by this cache registered mappings of message type names to their descriptors. 226 * 227 * @return An unmodifiable collection of all registered message type name to descriptor mappings 228 */ 229 public Collection<Map.Entry<String, Descriptors.Descriptor>> getEntries() { 230 return Collections.unmodifiableCollection(typeNameToDescriptor.entrySet()); 231 } 232 233 /** 234 * Whether the cache has no descriptors registered. 235 * 236 * @return True if the cache has no descriptors registered, false otherwise 237 */ 238 public boolean isEmpty() { 239 return typeNameToDescriptor.isEmpty(); 240 } 241 242 /** 243 * Gets how many descriptors are currently registered to the cache. 244 * 245 * @return The amount of descriptors registered to the cache 246 */ 247 public int size() { 248 return typeNameToDescriptor.size(); 249 } 250 }